#include	"core/mongoose.h"
#include	"core/storage.h"
#include	"utils/logger.h"
#include	"utils/param.h"
#include	"utils/system.h"
#include	"api/api_def.h"

#include	<cstdio>
#include	<csignal>
#include	<iostream>

#if !defined(_WIN32)
#	include		<sys/time.h>
#	include		<sys/resource.h>
#	include		<unistd.h>
#	include		<uuid/uuid.h>
#endif

struct MultiPartData {
	FILE *		pFile;
	std::string	sFiles;
};

static std::string UniqueName() {
#if defined(_WIN32)
	GUID		iGuid;
	uint32_t	pData[4];
	char		pHex[64];

	::CoInitialize(NULL);
	::CoCreateGuid(&iGuid);
	::CoUninitialize();

	::memset(pHex, 0, 64);
	::memcpy(pData, &iGuid, 16);

	snprintf(pHex, 64, "%08X%08X%08X%08X", pData[0], pData[1], pData[2], pData[3]);
	return std::string(pHex);
#else
	uuid_t		iUUID;
	uint32_t	pData[4];
	char		pHex[64];

	uuid_generate(iUUID);
	::memset(pHex, 0, 64);
	::memcpy(pData, iUUID, 16);

	snprintf(pHex, 64, "%08X%08X%08X%08X", pData[0], pData[1], pData[2], pData[3]);
	return std::string(pHex);
#endif
}

static void Call(mg_connection * p, http_message * pMsg) {
	lua_State * pL = luaL_newstate();
	luaL_openlibs(pL);

	extern void RegisterApi_Utils(lua_State *);
	extern void RegisterApi_Logger(lua_State *);
	extern void RegisterApi_Storage(lua_State *);
	extern void RegisterApi_Http(lua_State *);
	RegisterApi_Utils(pL);
	RegisterApi_Logger(pL);
	RegisterApi_Storage(pL);
	RegisterApi_Http(pL);

	uint32_t nRemote = (uint32_t)p->sa.sin.sin_addr.s_addr;
	char pBuf[16] = { 0 };
	snprintf(pBuf, 16, "%d.%d.%d.%d", (nRemote & 0xFF), (nRemote & 0xFF00) >> 8, (nRemote & 0xFF0000) >> 16, (nRemote & 0xFF000000) >> 24);

	std::string sUrl;
	sUrl.reserve(512);
	for (size_t i = 0; i < pMsg->uri.len; ++i) {
		char c = pMsg->uri.p[i];
		sUrl.push_back(c);

		if (c == '/' && i != pMsg->uri.len - 1) {
			while (pMsg->uri.p[i] == '/' && i < pMsg->uri.len) ++i;
			--i;
		}
	}

	lua_pushlightuserdata(pL, p);
	lua_setglobal(pL, "_CONN");
	lua_pushstring(pL, pBuf);
	lua_setglobal(pL, "_REMOTE");
	lua_pushlstring(pL, sUrl.data(), sUrl.size());
	lua_setglobal(pL, "_URL");
	lua_pushlstring(pL, pMsg->method.p, pMsg->method.len);
	lua_setglobal(pL, "_METHOD");

	auto n = sUrl.find_last_of('/');
	if (n == sUrl.size() - 1 || n == 0) {
		lua_pushlstring(pL, sUrl.data() + 1, n == 0 ? sUrl.size() - 1 : sUrl.size() - 2);
		lua_setglobal(pL, "_CONTROLLER");
		lua_pushlstring(pL, "index", 5);
		lua_setglobal(pL, "_ACTION");
	} else {
		lua_pushlstring(pL, sUrl.data() + 1, n - 1);
		lua_setglobal(pL, "_CONTROLLER");
		lua_pushlstring(pL, sUrl.data() + n + 1, sUrl.size() - 1 - n);
		lua_setglobal(pL, "_ACTION");
	}

	if (pMsg->query_string.len > 0) {
		char * pBuf = new char[pMsg->query_string.len + 1];
		mg_url_decode(pMsg->query_string.p, pMsg->query_string.len, pBuf, pMsg->query_string.len + 1, true);
		lua_pushstring(pL, pBuf);
		lua_setglobal(pL, "_GET_ORG");
	}

	if (pMsg->body.len > 0) {
		char * pBuf = new char[pMsg->body.len + 1];
		mg_url_decode(pMsg->body.p, pMsg->body.len, pBuf, pMsg->body.len + 1, true);
		lua_pushstring(pL, pBuf);
		lua_setglobal(pL, "_POST_ORG");
	}

	mg_str * pCookie = mg_get_http_header(pMsg, "cookie");
	if (pCookie) {
		lua_pushlstring(pL, pCookie->p, pCookie->len);
		lua_setglobal(pL, "_COOKIE");
	}

	luaL_dofile(pL, "./core/app.lua");
	lua_close(pL);
}

static void ServeFile(mg_connection * pConn, int nEvent, void * pData) {
	if (nEvent == MG_EV_HTTP_REQUEST) {
		mg_serve_http(pConn, (http_message *)pData, mg_serve_http_opts());
	}
}

static void UploadFile(mg_connection * pConn, int nEvent, void * pData) {
	if (nEvent == MG_EV_HTTP_REQUEST) {
		std::string sWarn = "<div style=\"text-align:center;margin-top:10%;margin-left:4%;margin-right:4%;\"><h2>403 Forbidden</h2><hr><p></p></div>";
		mg_printf(pConn, "HTTP/1.1 403 Forbidden.\r\nContent-Length:%d\r\n\r\n%s", sWarn.size(), sWarn.data());		
		pConn->flags |= MG_F_SEND_AND_CLOSE;
	} else if (nEvent == MG_EV_HTTP_MULTIPART_REQUEST) {
		http_message * pMsg = (http_message *)pData;
		MultiPartData * pMulti = new MultiPartData;
		if (pMulti == nullptr) {
			mg_printf(pConn, "HTTP/1.1 500 Failed to create file for upload.\r\nContent-Length:0\r\n\r\n");
			pConn->flags |= MG_F_SEND_AND_CLOSE;
			return;
		}

		pMulti->pFile = nullptr;
		pMulti->sFiles = "[";

		if (!Path::Exists("upload")) Path::Create("upload");
		pConn->user_data = pMulti;
	} else if (nEvent == MG_EV_HTTP_PART_BEGIN) {
		mg_http_multipart_part * pMsg = (mg_http_multipart_part *)pData;
		MultiPartData * pMulti = (MultiPartData *)pConn->user_data;

		std::string sPath = UniqueName();
		pMulti->pFile = fopen(("upload/" + sPath).c_str(), "wb");

		if (!pMulti->pFile) {
			delete pMulti;
			mg_printf(pConn, "HTTP/1.1 500 Failed to create file for upload.\r\nContent-Length:0\r\n\r\n");
			pConn->flags |= MG_F_SEND_AND_CLOSE;
			return;
		}

		pMulti->sFiles.append("{\"name\" : \"");
		pMulti->sFiles.append(pMsg->file_name);
		pMulti->sFiles.append("\", \"path\": \"");
		pMulti->sFiles.append(sPath);
		pMulti->sFiles.append("\"},");
	} else if (nEvent == MG_EV_HTTP_PART_DATA) {
		mg_http_multipart_part * pMsg = (mg_http_multipart_part *)pData;
		MultiPartData * pMulti = (MultiPartData *)pConn->user_data;
								
		if (fwrite(pMsg->data.p, 1, pMsg->data.len, pMulti->pFile) != pMsg->data.len) {
			delete pMulti;
			mg_printf(pConn, "HTTP/1.1 500 Failed to append data to file for upload.\r\nContent-Length:0\r\n\r\n");
			pConn->flags |= MG_F_SEND_AND_CLOSE;
		}
	} else if (nEvent == MG_EV_HTTP_PART_END) {
		mg_http_multipart_part * pMsg = (mg_http_multipart_part *)pData;
		MultiPartData * pMulti = (MultiPartData *)pConn->user_data;

		if (pMulti->pFile) {
			fflush(pMulti->pFile);
			fclose(pMulti->pFile);
			pMulti->pFile = nullptr;
		}
	} else if (nEvent == MG_EV_HTTP_MULTIPART_END) {
		mg_http_multipart_part * pMsg = (mg_http_multipart_part *)pData;
		MultiPartData * pMulti = (MultiPartData *)pConn->user_data;
		pConn->user_data = nullptr;

		if (pMulti->sFiles.size() >= 2) pMulti->sFiles.back() = ']';
		if (pMulti->pFile) {
			fflush(pMulti->pFile);
			fclose(pMulti->pFile);
			pMulti->pFile = nullptr;
		}
		
		mg_printf(pConn, 
			"HTTP/1.1 200 Upload finished.\r\n"
			"Content-type:application/json\r\n"
			"Content-Length:%d\r\n"
			"Connection:close\r\n\r\n"
			"%s", (int)pMulti->sFiles.size(), pMulti->sFiles.c_str());

		pConn->flags |= MG_F_SEND_AND_CLOSE;
		delete pMulti;
	}
}

static void ServeHTTP(mg_connection * pConn, int nEvent, void * pData) {
	if (nEvent == MG_EV_HTTP_REQUEST) {
		uint64_t nStart = Tick();
		http_message * pMsg = (http_message *)pData;
		Call(pConn, pMsg);

		std::string sUrl(pMsg->uri.p, pMsg->uri.len);
		std::string sMethod(pMsg->method.p, pMsg->method.len);
		LOG_DEBUG("%s\t%llums\t%s", sMethod.c_str(), Tick() - nStart, sUrl.c_str());
		mg_send_http_chunk(pConn, "", 0);
	}
}

int main(int nArgc, char * pArgv[]) {
	Param iParam(nArgc, pArgv);

	if (iParam.Has("-h")) {
		std::cout << "omni - Tiny MVC web server for LUA developer. \n"
			<< "Author : longshuang@msn.cn.\n"
			<< "\n[USAGE]\n"
			<< "-h\t\tShow this message.\n"
			<< "-v\t\tShow detail log message.\n"
			<< "-s=[N]\t\tSet session expire.\n"
			<< "-p=[N]\t\tSet listen port.\n"
			<< "-d\t\t Linux only. Run server as daemon application."
			<< std::endl;
		return 1;
	}

	bool bVerbose = iParam.Has("-v");
	std::string sPort = iParam.Has("-p") ? iParam.Get("-p") : "80";

	Logger::Get().Init("omni", "logs", 4194304, bVerbose ? ELog::Debug : ELog::Info);
	if (iParam.Has("-s")) Storage::Get().SetExpire(atoi(iParam.Get("-s").c_str()));
	
#if !defined(_WIN32)
	signal(SIGPIPE, SIG_IGN);

	struct rlimit iCore;
	getrlimit(RLIMIT_CORE, &iCore);
	iCore.rlim_cur = RLIM_INFINITY;
	iCore.rlim_max = RLIM_INFINITY;
	setrlimit(RLIMIT_CORE, &iCore);

	if (iParam.Has("-d") && daemon(1, 0) != 0) LOG_ERR("Failed run application as daemon application!");
#endif
	
	struct mg_mgr			iMgr;
	struct mg_connection *	pConn;

	mg_mgr_init(&iMgr, 0);
	pConn = mg_bind(&iMgr, sPort.c_str(), ServeHTTP);
	if (!pConn) {
		LOG_ERR("Failed to run omni on %s", sPort.c_str());
		return 1;
	}

	mg_set_protocol_http_websocket(pConn);
	mg_register_http_endpoint(pConn, "/public", ServeFile);
	mg_register_http_endpoint(pConn, "/upload", UploadFile);
	mg_register_http_endpoint(pConn, "/favicon.ico", ServeFile);
	mg_enable_multithreading(pConn);
	LOG_INFO("Starting server on :%s", sPort.c_str());

	while (true) {
		try {
			mg_mgr_poll(&iMgr, 1);
		} catch (...) {}		
	}

	mg_mgr_free(&iMgr);
	return 0;
}